const std = @import("std");
const assert = std.debug.assert;
const log = std.log.scoped(.ip);
const net = @import("../net.zig");
const NetBitOrder = net.NetBitOrder;
const Ether = net.ether.Ether;

/// an IP packet header (comes after an Ethernet header).
pub const Ip = extern struct {
    vhl: NetBitOrder(Vhl) align(1) = .{},
    /// type of service
    tos: u8 align(1) = 0,
    /// total length, including this IP header, expressd in bytes
    total: u16 align(1),
    /// identification
    id: u16 align(1) = 0,
    /// fragment offset field
    offset: u16 align(1) = 0,
    /// time to live
    ttl: u8 align(1) = 100,
    protocol: Protocol align(1),
    /// checksum, covers just IP header
    sum: u16 align(1) = 0,
    src: u32 align(1) = net.local_ip,
    dst: u32 align(1),

    pub const Vhl = packed struct(u8) {
        version: u4 = 4,
        /// length of the header, expressd in units of 32 bits
        header: u4 = 5,
    };

    pub const Protocol = enum(u8) {
        /// Control message protocol
        icmp = 1,
        /// Transmission control protocol
        tcp = 6,
        /// User datagram protocol
        udp = 17,
        _,
    };

    comptime {
        assert(@alignOf(@This()) == 1);
        assert(@bitSizeOf(@This()) / 8 == @sizeOf(@This()));
    }

    pub const header_size = @sizeOf(@This());
    pub const packet_lenth = Ether.packet_lenth + header_size;

    // https://www.saminiir.com/lets-code-tcp-ip-stack-2-ipv4-icmpv4
    pub fn checksum(self: Ip) u16 {
        var csum: u32 = 0;
        const words = std.mem.bytesAsSlice(u16, std.mem.asBytes(&self));
        for (words) |w| csum += w;
        while (csum >> 16 != 0) {
            csum = (csum & 0xffff) + (csum >> 16);
        }
        return @truncate(~csum);
    }
};

pub fn send(packet: []u8, p: Ip.Protocol, dst: u32, len: usize) !void {
    const ip: *Ip = @ptrCast(packet[Ether.packet_lenth..]);
    ip.* = .{
        .total = @intCast(Ip.header_size + len),
        .protocol = p,
        .dst = dst,
    };
    net.swapAllFields(ip);
    ip.sum = ip.checksum();

    try net.ether.send(packet, .ip, net.host_mac);
}

pub fn recv(packet: []u8, eth: *Ether) !void {
    const S = struct {
        var seen_ip: bool = false;
    };

    if (S.seen_ip) return;

    log.info("received an IP packet", .{});
    S.seen_ip = true;

    const ip: *Ip = @ptrCast(packet[Ether.packet_lenth..]);
    net.swapAllFields(ip);
    _ = eth;
}
